// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/kraken/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/base/order/rate_config.h"
#include "coin2/exchange/kraken/api_util/rate_tracker.h"
#include "coin2/exchange/kraken/symbology/product.h"

using coin2::exchange::kraken::symbology::KrakenProduct;

inline int32_t ProcOrderIdToUserRef(int64_t proc_order_id) {
  int32_t* x = reinterpret_cast<int32_t*>(&proc_order_id);
  return x[0] + x[1];
}

auto KrakenNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  params.set_pair(order_spec.product);
  params.set_userref(std::to_string(ProcOrderIdToUserRef(proc_order_id)));

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_type("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_type("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  KrakenProduct product = KrakenProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_volume(formatter.FormatQty(order_spec.qty));
  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_ordertype("market");
  } else {
    params.set_ordertype("limit");
    params.set_price(formatter.FormatPrice(order_spec.price));

    if (order_spec.post_only) {
      params.set_oflags("post");
    }
  }
  return params;
}

auto KrakenNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();

  params.set_pair(order_context.native_product());
  if (order_context.has_exchange_order_id()) {
    params.set_txid(order_context.exchange_order_id());
  } else {
    params.set_txid(std::to_string(ProcOrderIdToUserRef(order_context.proc_order_id())));
  }
  return params;
}

KrakenNativePrivateClient::KrakenNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("api.kraken.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[KrakenNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<KrakenRestAuth, NativeClientConfig>(config);

  rate_tracker_ = std::make_unique<KrakenRateTracker>(mea_, config);
  local_rate_tracker_ = static_cast<KrakenRateTracker*>(rate_tracker_.get());
}

void KrakenNativePrivateClient::Prepare() {
  auto* tracker = dynamic_cast<KrakenRateTracker*>(this->rate_tracker_.get());
  mb_->Subscribe("kraken/order_rate_limit", [this, tracker](const std::string&, void* data) {
    auto json = nlohmann::json::parse(*reinterpret_cast<std::string*>(data));
    tracker->UpdateReqRateInfo(json);
  });
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::QueryAccountBalance() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/Balance", &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::QueryOpenOrders() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/OpenOrders", &builder);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::QueryClosedOrders() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/ClosedOrders", &builder);

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::QueryTrades() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/TradesHistory", &builder);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::PlaceOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("pair", params.pair());
  builder.PutPost("ordertype", params.ordertype());
  builder.PutPost("type", params.type());
  builder.PutPost("price", params.price());
  builder.PutPost("volume", params.volume());
  builder.PutPost("oflags", params.oflags());
  builder.PutPost("userref", params.userref());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/AddOrder", &builder);
  auto ts = GetCurrentTimestamp();
  Push(1, ts, ActionType::PLACE, params.pair());

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

auto KrakenNativePrivateClient::GetCancelOrderRateLimitWeight(
    int64_t timestamp,
    int64_t order_created_ts) const -> int {
  auto order_life_time = timestamp - order_created_ts;
  if (order_life_time >= 300'000'000'000LL) {
    return 0;
  } else if (order_life_time >= 90'000'000'000LL) {
    return 1;
  } else if (order_life_time >= 45'000'000'000LL) {
    return 2;
  } else if (order_life_time >= 15'000'000'000LL) {
    return 4;
  } else if (order_life_time >= 10'000'000'000LL) {
    return 5;
  } else if (order_life_time >= 5'000'000'000LL) {
    return 6;
  } else {
    return 8;
  }
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::CancelOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("txid", params.txid());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/CancelOrder", &builder);
  auto ts = GetCurrentTimestamp();
  Push(8, ts, ActionType::CANCEL, params.pair());

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::CancelOrder(
    const RestParams& params,
    int64_t order_created_ts) {
  UrlParamsBuilder builder;
  builder.PutPost("txid", params.txid());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/0/private/CancelOrder", &builder);
  auto ts = GetCurrentTimestamp();
  auto weight = GetCancelOrderRateLimitWeight(ts, order_created_ts);
  Push(weight, ts, ActionType::CANCEL, params.pair());

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::GetWsToken() {
  UrlParamsBuilder builder;
  auto* context =
      GetFirstAuth()->CreateRequestByPostWithSignature("/0/private/GetWebSocketsToken", &builder);

  context->query_type = OrderSubTopicId::LISTEN_KEY;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params, order_exe.oc->order_created_time());
}

bool KrakenNativePrivateClient::HasEnoughRateLimit(
    int64_t ts,
    ActionType action_type,
    const std::string& symbol) const {
  if (!local_rate_tracker_) {
    return true;
  }
  auto ret = local_rate_tracker_->HasEnoughRateLimit(ts, action_type, symbol);
  if (!ret && order_logger_cb_) {
    rate_tracker_->WriteRateLimitReportMessage(order_logger_cb_);
  }

  return ret;
}

void KrakenNativePrivateClient::Push(
    int32_t num_reqs,
    int64_t ts,
    ActionType action_type,
    const std::string& symbol) {
  if (!local_rate_tracker_) {
    return;
  }
  local_rate_tracker_->Push(num_reqs, ts, action_type, symbol);
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> KrakenNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders();
    }
    default: {
      return nullptr;
    }
  }
}
